/*-------------------------------------------------------------------------
 * drawElements Quality Program Execution Server
 * ---------------------------------------------
 *
 * Copyright 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *//*!
 * \file
 * \brief TestProcess implementation for Win32.
 *//*--------------------------------------------------------------------*/

#include "xsWin32TestProcess.hpp"
#include "deFilePath.hpp"
#include "deString.h"
#include "deMemory.h"
#include "deClock.h"
#include "deFile.h"

#include <sstream>
#include <string.h>

using std::string;
using std::vector;

namespace xs
{

enum
{
	MAX_OLD_LOGFILE_DELETE_ATTEMPTS		= 20,	//!< How many times execserver tries to delete old log file
	LOGFILE_DELETE_SLEEP_MS				= 50	//!< Sleep time (in ms) between log file delete attempts
};

namespace win32
{

// Error

static std::string formatErrMsg (DWORD error, const char* msg)
{
	std::ostringstream	str;
	LPSTR				msgBuf;

#if defined(UNICODE)
#	error Unicode not supported.
#endif

	if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,
					  NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&msgBuf, 0, DE_NULL) > 0)
		str << msg << ", error " << error << ": " << msgBuf;
	else
		str << msg << ", error " << error;

	return str.str();
}

Error::Error (DWORD error, const char* msg)
	: std::runtime_error(formatErrMsg(error, msg))
	, m_error			(error)
{
}

// Event

Event::Event (bool manualReset, bool initialState)
	: m_handle(0)
{
	m_handle = CreateEvent(NULL, manualReset ? TRUE : FALSE, initialState ? TRUE : FALSE, NULL);
	if (!m_handle)
		throw Error(GetLastError(), "CreateEvent() failed");
}

Event::~Event (void)
{
	CloseHandle(m_handle);
}

void Event::setSignaled (void)
{
	if (!SetEvent(m_handle))
		throw Error(GetLastError(), "SetEvent() failed");
}

void Event::reset (void)
{
	if (!ResetEvent(m_handle))
		throw Error(GetLastError(), "ResetEvent() failed");
}

// CaseListWriter

CaseListWriter::CaseListWriter (void)
	: m_dst			(INVALID_HANDLE_VALUE)
	, m_cancelEvent	(true, false)
{
}

CaseListWriter::~CaseListWriter (void)
{
}

void CaseListWriter::start (const char* caseList, HANDLE dst)
{
	DE_ASSERT(!isStarted());

	m_dst = dst;

	int caseListSize = (int)strlen(caseList)+1;
	m_caseList.resize(caseListSize);
	std::copy(caseList, caseList+caseListSize, m_caseList.begin());

	de::Thread::start();
}

void CaseListWriter::run (void)
{
	try
	{
		Event		ioEvent			(true, false); // Manual reset, non-signaled state.
		HANDLE		waitHandles[]	= { ioEvent.getHandle(), m_cancelEvent.getHandle() };
		OVERLAPPED	overlapped;
		int			curPos = 0;

		deMemset(&overlapped, 0, sizeof(overlapped));
		overlapped.hEvent = ioEvent.getHandle();

		while (curPos < (int)m_caseList.size())
		{
			const int	maxWriteSize	= 4096;
			const int	numToWrite		= de::min(maxWriteSize, (int)m_caseList.size() - curPos);
			DWORD		waitRes			= 0;

			if (!WriteFile(m_dst, &m_caseList[curPos], (DWORD)numToWrite, NULL, &overlapped))
			{
				DWORD err = GetLastError();
				if (err != ERROR_IO_PENDING)
					throw Error(err, "WriteFile() failed");
			}

			waitRes = WaitForMultipleObjects(DE_LENGTH_OF_ARRAY(waitHandles), &waitHandles[0], FALSE, INFINITE);

			if (waitRes == WAIT_OBJECT_0)
			{
				DWORD numBytesWritten = 0;

				// \note GetOverlappedResult() will fail with ERROR_IO_INCOMPLETE if IO event is not complete (should be).
				if (!GetOverlappedResult(m_dst, &overlapped, &numBytesWritten, FALSE))
					throw Error(GetLastError(), "GetOverlappedResult() failed");

				if (numBytesWritten == 0)
					throw Error(GetLastError(), "Writing to pipe failed (pipe closed?)");

				curPos += (int)numBytesWritten;
			}
			else if (waitRes == WAIT_OBJECT_0 + 1)
			{
				// Cancel.
				if (!CancelIo(m_dst))
					throw Error(GetLastError(), "CancelIo() failed");
				break;
			}
			else
				throw Error(GetLastError(), "WaitForMultipleObjects() failed");
		}
	}
	catch (const std::exception& e)
	{
		// \todo [2013-08-13 pyry] What to do about this?
		printf("win32::CaseListWriter::run(): %s\n", e.what());
	}
}

void CaseListWriter::stop (void)
{
	if (!isStarted())
		return; // Nothing to do.

	m_cancelEvent.setSignaled();

	// Join thread.
	join();

	m_cancelEvent.reset();

	m_dst = INVALID_HANDLE_VALUE;
}

// FileReader

FileReader::FileReader (ThreadedByteBuffer* dst)
	: m_dstBuf		(dst)
	, m_handle		(INVALID_HANDLE_VALUE)
	, m_cancelEvent	(false, false)
{
}

FileReader::~FileReader (void)
{
}

void FileReader::start (HANDLE file)
{
	DE_ASSERT(!isStarted());

	m_handle = file;

	de::Thread::start();
}

void FileReader::run (void)
{
	try
	{
		Event					ioEvent			(true, false); // Manual reset, not signaled state.
		HANDLE					waitHandles[]	= { ioEvent.getHandle(), m_cancelEvent.getHandle() };
		OVERLAPPED				overlapped;
		std::vector<deUint8>	tmpBuf			(FILEREADER_TMP_BUFFER_SIZE);
		deUint64				offset			= 0; // Overlapped IO requires manual offset keeping.

		deMemset(&overlapped, 0, sizeof(overlapped));
		overlapped.hEvent = ioEvent.getHandle();

		for (;;)
		{
			DWORD	numBytesRead	= 0;
			DWORD	waitRes;

			overlapped.Offset		= (DWORD)(offset & 0xffffffffu);
			overlapped.OffsetHigh	= (DWORD)(offset >> 32);

			if (!ReadFile(m_handle, &tmpBuf[0], (DWORD)tmpBuf.size(), NULL, &overlapped))
			{
				DWORD err = GetLastError();

				if (err == ERROR_BROKEN_PIPE)
					break;
				else if (err == ERROR_HANDLE_EOF)
				{
					if (m_dstBuf->isCanceled())
						break;

					deSleep(FILEREADER_IDLE_SLEEP);

					if (m_dstBuf->isCanceled())
						break;
					else
						continue;
				}
				else if (err != ERROR_IO_PENDING)
					throw Error(err, "ReadFile() failed");
			}

			waitRes = WaitForMultipleObjects(DE_LENGTH_OF_ARRAY(waitHandles), &waitHandles[0], FALSE, INFINITE);

			if (waitRes == WAIT_OBJECT_0)
			{
				// \note GetOverlappedResult() will fail with ERROR_IO_INCOMPLETE if IO event is not complete (should be).
				if (!GetOverlappedResult(m_handle, &overlapped, &numBytesRead, FALSE))
				{
					DWORD err = GetLastError();

					if (err == ERROR_HANDLE_EOF)
					{
						// End of file - for now.
						// \note Should check for end of buffer here, or otherwise may end up in infinite loop.
						if (m_dstBuf->isCanceled())
							break;

						deSleep(FILEREADER_IDLE_SLEEP);

						if (m_dstBuf->isCanceled())
							break;
						else
							continue;
					}
					else if (err == ERROR_BROKEN_PIPE)
						break;
					else
						throw Error(err, "GetOverlappedResult() failed");
				}

				if (numBytesRead == 0)
					throw Error(GetLastError(), "Reading from file failed");
				else
					offset += (deUint64)numBytesRead;
			}
			else if (waitRes == WAIT_OBJECT_0 + 1)
			{
				// Cancel.
				if (!CancelIo(m_handle))
					throw Error(GetLastError(), "CancelIo() failed");
				break;
			}
			else
				throw Error(GetLastError(), "WaitForMultipleObjects() failed");

			try
			{
				m_dstBuf->write((int)numBytesRead, &tmpBuf[0]);
				m_dstBuf->flush();
			}
			catch (const ThreadedByteBuffer::CanceledException&)
			{
				// Canceled.
				break;
			}
		}
	}
	catch (const std::exception& e)
	{
		// \todo [2013-08-13 pyry] What to do?
		printf("win32::FileReader::run(): %s\n", e.what());
	}
}

void FileReader::stop (void)
{
	if (!isStarted())
		return; // Nothing to do.

	m_cancelEvent.setSignaled();

	// Join thread.
	join();

	m_cancelEvent.reset();

	m_handle = INVALID_HANDLE_VALUE;
}

// TestLogReader

TestLogReader::TestLogReader (void)
	: m_logBuffer	(LOG_BUFFER_BLOCK_SIZE, LOG_BUFFER_NUM_BLOCKS)
	, m_logFile		(INVALID_HANDLE_VALUE)
	, m_reader		(&m_logBuffer)
{
}

TestLogReader::~TestLogReader (void)
{
	if (m_logFile != INVALID_HANDLE_VALUE)
		CloseHandle(m_logFile);
}

void TestLogReader::start (const char* filename)
{
	DE_ASSERT(m_logFile == INVALID_HANDLE_VALUE && !m_reader.isStarted());

	m_logFile = CreateFile(filename,
						   GENERIC_READ,
						   FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
						   DE_NULL,
						   OPEN_EXISTING,
						   FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,
						   DE_NULL);

	if (m_logFile == INVALID_HANDLE_VALUE)
		throw Error(GetLastError(), "Failed to open log file");

	m_reader.start(m_logFile);
}

void TestLogReader::stop (void)
{
	if (!m_reader.isStarted())
		return; // Nothing to do.

	m_logBuffer.cancel();
	m_reader.stop();

	CloseHandle(m_logFile);
	m_logFile = INVALID_HANDLE_VALUE;

	m_logBuffer.clear();
}

// Process

Process::Process (void)
	: m_state		(STATE_NOT_STARTED)
	, m_exitCode	(0)
	, m_standardIn	(INVALID_HANDLE_VALUE)
	, m_standardOut	(INVALID_HANDLE_VALUE)
	, m_standardErr	(INVALID_HANDLE_VALUE)
{
	deMemset(&m_procInfo, 0, sizeof(m_procInfo));
}

Process::~Process (void)
{
	try
	{
		if (isRunning())
		{
			kill();
			waitForFinish();
		}
	}
	catch (...)
	{
	}

	cleanupHandles();
}

void Process::cleanupHandles (void)
{
	DE_ASSERT(!isRunning());

	if (m_standardErr != INVALID_HANDLE_VALUE)
		CloseHandle(m_standardErr);

	if (m_standardOut != INVALID_HANDLE_VALUE)
		CloseHandle(m_standardOut);

	if (m_standardIn != INVALID_HANDLE_VALUE)
		CloseHandle(m_standardIn);

	if (m_procInfo.hProcess)
		CloseHandle(m_procInfo.hProcess);

	if (m_procInfo.hThread)
		CloseHandle(m_procInfo.hThread);

	m_standardErr	= INVALID_HANDLE_VALUE;
	m_standardOut	= INVALID_HANDLE_VALUE;
	m_standardIn	= INVALID_HANDLE_VALUE;

	deMemset(&m_procInfo, 0, sizeof(m_procInfo));
}

__declspec(thread) static int t_pipeNdx = 0;

static void createPipeWithOverlappedIO (HANDLE* readHandleOut, HANDLE* writeHandleOut, deUint32 readMode, deUint32 writeMode, SECURITY_ATTRIBUTES* securityAttr)
{
	const int	defaultBufSize	= 4096;
	char		pipeName[128];
	HANDLE		readHandle;
	HANDLE		writeHandle;

	DE_ASSERT(((readMode | writeMode) & ~FILE_FLAG_OVERLAPPED) == 0);

	deSprintf(pipeName, sizeof(pipeName), "\\\\.\\Pipe\\dEQP-ExecServer-%08x-%08x-%08x",
			  GetCurrentProcessId(),
			  GetCurrentThreadId(),
			  t_pipeNdx++);

	readHandle = CreateNamedPipe(pipeName,						/* Pipe name.				*/
								 PIPE_ACCESS_INBOUND|readMode,	/* Open mode.				*/
								 PIPE_TYPE_BYTE|PIPE_WAIT,		/* Pipe flags.				*/
								 1,								/* Max number of instances.	*/
								 defaultBufSize,				/* Output buffer size.		*/
								 defaultBufSize,				/* Input buffer size.		*/
								 0,								/* Use default timeout.		*/
								 securityAttr);

	if (readHandle == INVALID_HANDLE_VALUE)
		throw Error(GetLastError(), "CreateNamedPipe() failed");

	writeHandle = CreateFile(pipeName,
							 GENERIC_WRITE,						/* Access mode.				*/
							 0,									/* No sharing.				*/
							 securityAttr,
							 OPEN_EXISTING,						/* Assume existing object.	*/
							 FILE_ATTRIBUTE_NORMAL|writeMode,	/* Open mode / flags.		*/
							 DE_NULL							/* Template file.			*/);

	if (writeHandle == INVALID_HANDLE_VALUE)
	{
		DWORD openErr = GetLastError();
		CloseHandle(readHandle);
		throw Error(openErr, "Failed to open created pipe, CreateFile() failed");
	}

	*readHandleOut	= readHandle;
	*writeHandleOut	= writeHandle;
}

void Process::start (const char* commandLine, const char* workingDirectory)
{
	// Pipes.
	HANDLE		stdInRead	= INVALID_HANDLE_VALUE;
	HANDLE		stdInWrite	= INVALID_HANDLE_VALUE;
	HANDLE		stdOutRead	= INVALID_HANDLE_VALUE;
	HANDLE		stdOutWrite	= INVALID_HANDLE_VALUE;
	HANDLE		stdErrRead	= INVALID_HANDLE_VALUE;
	HANDLE		stdErrWrite	= INVALID_HANDLE_VALUE;

	if (m_state == STATE_RUNNING)
		throw std::runtime_error("Process already running");
	else if (m_state == STATE_FINISHED)
	{
		// Process finished, clean up old cruft.
		cleanupHandles();
		m_state = STATE_NOT_STARTED;
	}

	// Create pipes
	try
	{
		SECURITY_ATTRIBUTES	securityAttr;
		STARTUPINFO			startInfo;

		deMemset(&startInfo, 0, sizeof(startInfo));
		deMemset(&securityAttr, 0, sizeof(securityAttr));

		// Security attributes for inheriting handle.
		securityAttr.nLength				= sizeof(SECURITY_ATTRIBUTES);
		securityAttr.bInheritHandle			= TRUE;
		securityAttr.lpSecurityDescriptor	= DE_NULL;

		createPipeWithOverlappedIO(&stdInRead,	&stdInWrite,	0, FILE_FLAG_OVERLAPPED, &securityAttr);
		createPipeWithOverlappedIO(&stdOutRead,	&stdOutWrite,	FILE_FLAG_OVERLAPPED, 0, &securityAttr);
		createPipeWithOverlappedIO(&stdErrRead,	&stdErrWrite,	FILE_FLAG_OVERLAPPED, 0, &securityAttr);

		if (!SetHandleInformation(stdInWrite, HANDLE_FLAG_INHERIT, 0) ||
			!SetHandleInformation(stdOutRead, HANDLE_FLAG_INHERIT, 0) ||
			!SetHandleInformation(stdErrRead, HANDLE_FLAG_INHERIT, 0))
			throw Error(GetLastError(), "SetHandleInformation() failed");

		// Startup info for process.
		startInfo.cb			= sizeof(startInfo);
		startInfo.hStdError		 = stdErrWrite;
		startInfo.hStdOutput	 = stdOutWrite;
		startInfo.hStdInput		 = stdInRead;
		startInfo.dwFlags		|= STARTF_USESTDHANDLES;

		if (!CreateProcess(DE_NULL, (LPTSTR)commandLine, DE_NULL, DE_NULL, TRUE /* inherit handles */, 0, DE_NULL, workingDirectory, &startInfo, &m_procInfo))
			throw Error(GetLastError(), "CreateProcess() failed");
	}
	catch (...)
	{
		if (stdInRead	!= INVALID_HANDLE_VALUE)	CloseHandle(stdInRead);
		if (stdInWrite	!= INVALID_HANDLE_VALUE)	CloseHandle(stdInWrite);
		if (stdOutRead	!= INVALID_HANDLE_VALUE)	CloseHandle(stdOutRead);
		if (stdOutWrite	!= INVALID_HANDLE_VALUE)	CloseHandle(stdOutWrite);
		if (stdErrRead	!= INVALID_HANDLE_VALUE)	CloseHandle(stdErrRead);
		if (stdErrWrite	!= INVALID_HANDLE_VALUE)	CloseHandle(stdErrWrite);
		throw;
	}

	// Store handles to be kept.
	m_standardIn	= stdInWrite;
	m_standardOut	= stdOutRead;
	m_standardErr	= stdErrRead;

	// Close other ends of handles.
	CloseHandle(stdErrWrite);
	CloseHandle(stdOutWrite);
	CloseHandle(stdInRead);

	m_state = STATE_RUNNING;
}

bool Process::isRunning (void)
{
	if (m_state == STATE_RUNNING)
	{
		int exitCode;
		BOOL result = GetExitCodeProcess(m_procInfo.hProcess, (LPDWORD)&exitCode);

		if (result != TRUE)
			throw Error(GetLastError(), "GetExitCodeProcess() failed");

		if (exitCode == STILL_ACTIVE)
			return true;
		else
		{
			// Done.
			m_exitCode	= exitCode;
			m_state		= STATE_FINISHED;
			return false;
		}
	}
	else
		return false;
}

void Process::waitForFinish (void)
{
	if (m_state == STATE_RUNNING)
	{
		if (WaitForSingleObject(m_procInfo.hProcess, INFINITE) != WAIT_OBJECT_0)
			throw Error(GetLastError(), "Waiting for process failed, WaitForSingleObject() failed");

		if (isRunning())
			throw std::runtime_error("Process is still alive");
	}
	else
		throw std::runtime_error("Process is not running");
}

void Process::stopProcess (bool kill)
{
	if (m_state == STATE_RUNNING)
	{
		if (!TerminateProcess(m_procInfo.hProcess, kill ? -1 : 0))
			throw Error(GetLastError(), "TerminateProcess() failed");
	}
	else
		throw std::runtime_error("Process is not running");
}

void Process::terminate (void)
{
	stopProcess(false);
}

void Process::kill (void)
{
	stopProcess(true);
}

} // win32

Win32TestProcess::Win32TestProcess (void)
	: m_process				(DE_NULL)
	, m_processStartTime	(0)
	, m_infoBuffer			(INFO_BUFFER_BLOCK_SIZE, INFO_BUFFER_NUM_BLOCKS)
	, m_stdOutReader		(&m_infoBuffer)
	, m_stdErrReader		(&m_infoBuffer)
{
}

Win32TestProcess::~Win32TestProcess (void)
{
	delete m_process;
}

void Win32TestProcess::start (const char* name, const char* params, const char* workingDir, const char* caseList)
{
	bool hasCaseList = strlen(caseList) > 0;

	XS_CHECK(!m_process);

	de::FilePath logFilePath = de::FilePath::join(workingDir, "TestResults.qpa");
	m_logFileName = logFilePath.getPath();

	// Remove old file if such exists.
	// \note Sometimes on Windows the test process dies slowly and may not release handle to log file
	//		 until a bit later.
	// \todo [2013-07-15 pyry] This should be solved by improving deProcess and killing all child processes as well.
	{
		int tryNdx = 0;
		while (tryNdx < MAX_OLD_LOGFILE_DELETE_ATTEMPTS && deFileExists(m_logFileName.c_str()))
		{
			if (deDeleteFile(m_logFileName.c_str()))
				break;
			deSleep(LOGFILE_DELETE_SLEEP_MS);
			tryNdx += 1;
		}

		if (deFileExists(m_logFileName.c_str()))
			throw TestProcessException(string("Failed to remove '") + m_logFileName + "'");
	}

	// Construct command line.
	string cmdLine = de::FilePath(name).isAbsolutePath() ? name : de::FilePath::join(workingDir, name).normalize().getPath();
	cmdLine += string(" --deqp-log-filename=") + logFilePath.getBaseName();

	if (hasCaseList)
		cmdLine += " --deqp-stdin-caselist";

	if (strlen(params) > 0)
		cmdLine += string(" ") + params;

	DE_ASSERT(!m_process);
	m_process = new win32::Process();

	try
	{
		m_process->start(cmdLine.c_str(), strlen(workingDir) > 0 ? workingDir : DE_NULL);
	}
	catch (const std::exception& e)
	{
		delete m_process;
		m_process = DE_NULL;
		throw TestProcessException(e.what());
	}

	m_processStartTime = deGetMicroseconds();

	// Create stdout & stderr readers.
	m_stdOutReader.start(m_process->getStdOut());
	m_stdErrReader.start(m_process->getStdErr());

	// Start case list writer.
	if (hasCaseList)
		m_caseListWriter.start(caseList, m_process->getStdIn());
}

void Win32TestProcess::terminate (void)
{
	if (m_process)
	{
		try
		{
			m_process->kill();
		}
		catch (const std::exception& e)
		{
			printf("Win32TestProcess::terminate(): Failed to kill process: %s\n", e.what());
		}
	}
}

void Win32TestProcess::cleanup (void)
{
	m_caseListWriter.stop();

	// \note Buffers must be canceled before stopping readers.
	m_infoBuffer.cancel();

	m_stdErrReader.stop();
	m_stdOutReader.stop();
	m_testLogReader.stop();

	// Reset buffers.
	m_infoBuffer.clear();

	if (m_process)
	{
		try
		{
			if (m_process->isRunning())
			{
				m_process->kill();
				m_process->waitForFinish();
			}
		}
		catch (const std::exception& e)
		{
			printf("Win32TestProcess::cleanup(): Failed to kill process: %s\n", e.what());
		}

		delete m_process;
		m_process = DE_NULL;
	}
}

int Win32TestProcess::readTestLog (deUint8* dst, int numBytes)
{
	if (!m_testLogReader.isRunning())
	{
		if (deGetMicroseconds() - m_processStartTime > LOG_FILE_TIMEOUT*1000)
		{
			// Timeout, kill process.
			terminate();
			return 0; // \todo [2013-08-13 pyry] Throw exception?
		}

		if (!deFileExists(m_logFileName.c_str()))
			return 0;

		// Start reader.
		m_testLogReader.start(m_logFileName.c_str());
	}

	DE_ASSERT(m_testLogReader.isRunning());
	return m_testLogReader.read(dst, numBytes);
}

bool Win32TestProcess::isRunning (void)
{
	if (m_process)
		return m_process->isRunning();
	else
		return false;
}

int Win32TestProcess::getExitCode (void) const
{
	if (m_process)
		return m_process->getExitCode();
	else
		return -1;
}

} // xs
